﻿(function ($) {

    function makeMap(items) {
        var obj = {};
        
        for (var i = 0; i < items.length; i++)
            obj[items[i]] = true;
        
        return obj;
    }

    var empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed'.split(','));
    var blockElements = 'div,p,address,applet,blockquote,button,center,dd,del,dir,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,,pre,script,table,tbody,td,tfoot,th,thead,tr,ul'.split(',');
    var block = makeMap(blockElements);
    var inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'.split(','));
    var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'.split(','));

    var START_TO_START = 0,
        START_TO_END = 1,
        END_TO_END = 2,
        END_TO_START = 3;

    function W3CRange(doc) {
        $.extend(this, {
            ownerDocument: doc, /* not part of the spec; used when cloning ranges, traversing the dom and creating fragments */
            startContainer: doc,
            endContainer: doc,
            commonAncestorContainer: doc,
            startOffset: 0,
            endOffset: 0,
            collapsed: true
        });
    };

    W3CRange.prototype = {
        // Positioning Methods

        setStart: function (node, offset) {
            this.startContainer = node;
            this.startOffset = offset;
            updateRangeProperties(this);
            fixIvalidRange(this, true);
        },

        setEnd: function (node, offset) {
            this.endContainer = node;
            this.endOffset = offset;
            updateRangeProperties(this);
            fixIvalidRange(this, false);
        },

        setStartBefore: function (node) {
            this.setStart(node.parentNode, findNodeIndex(node));
        },

        setStartAfter: function (node) {
            this.setStart(node.parentNode, findNodeIndex(node) + 1);
        },

        setEndBefore: function (node) {
            this.setEnd(node.parentNode, findNodeIndex(node));
        },

        setEndAfter: function (node) {
            this.setEnd(node.parentNode, findNodeIndex(node) + 1);
        },

        selectNode: function (node) {
            this.setStartBefore(node);
            this.setEndAfter(node);
        },

        selectNodeContents: function (node) {
            this.setStart(node, 0);
            this.setEnd(node, node[node.nodeType === 1 ? 'childNodes' : 'nodeValue'].length);
        },

        collapse: function (toStart) {
            if (toStart)
                this.setEnd(this.startContainer, this.startOffset);
            else
                this.setStart(this.endContainer, this.endOffset);
        },

        // Editing Methods

        deleteContents: function () {
            var range = this.cloneRange();

            if (this.startContainer != this.commonAncestorContainer)
                this.setStartAfter(findClosestAncestor(this.commonAncestorContainer, this.startContainer));

            this.collapse(true);

            (function deleteSubtree(iterator) {
                while (iterator.next())
                    iterator.hasPartialSubtree() ? deleteSubtree(iterator.getSubtreeIterator())
                                             : iterator.remove();
            })(new RangeIterator(range));
        },

        extractContents: function () {
            var range = this.cloneRange();

            if (this.startContainer != this.commonAncestorContainer)
                this.setStartAfter(findClosestAncestor(this.commonAncestorContainer, this.startContainer));

            this.collapse(true);

            var self = this;

            var document = documentFromRange(this);

            return (function extractSubtree(iterator) {
                for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) {
                    iterator.hasPartialSubtree() ? node = node.cloneNode(false) : iterator.remove(self.originalRange);

                    if (iterator.hasPartialSubtree())
                        node.appendChild(extractSubtree(iterator.getSubtreeIterator()));

                    frag.appendChild(node);
                }

                return frag;
            })(new RangeIterator(range));
        },

        insertNode: function (node) {
            if (isDataNode(this.startContainer)) {
                if (this.startOffset != this.startContainer.nodeValue.length)
                    splitDataNode(this.startContainer, this.startOffset);

                dom.insertAfter(node, this.startContainer);
            } else {
                dom.insertAt(this.startContainer, node, this.startOffset);
            }

            this.setStart(this.startContainer, this.startOffset);
        },

        cloneRange: function () {
            // fast copy
            return $.extend(new W3CRange(this.ownerDocument), {
                startContainer: this.startContainer,
                endContainer: this.endContainer,
                commonAncestorContainer: this.commonAncestorContainer,
                startOffset: this.startOffset,
                endOffset: this.endOffset,
                collapsed: this.collapsed,

                originalRange: this /* not part of the spec; used to update the original range when calling extractContents() on clones */
            });
        },

        // used for debug purposes
        toString: function () {
            var startNodeName = this.startContainer.nodeName,
                endNodeName = this.endContainer.nodeName;

            return [startNodeName == "#text" ? this.startContainer.nodeValue : startNodeName, '(', this.startOffset, ') : ',
                    endNodeName == "#text" ? this.endContainer.nodeValue : endNodeName, '(', this.endOffset, ')'].join('');
        }
    }

    /* can be used in Range.compareBoundaryPoints if we need it one day */
    function compareBoundaries(start, end, startOffset, endOffset) {
        if (start == end)
            return endOffset - startOffset;

        // end is child of start
        var container = end;
        while (container && container.parentNode != start)
            container = container.parentNode;

        if (container)
            return findNodeIndex(container) - startOffset;

        // start is child of end
        container = start;
        while (container && container.parentNode != end)
            container = container.parentNode;

        if (container)
            return endOffset - findNodeIndex(container) - 1;

        // deep traversal
        var root = dom.commonAncestor(start, end);
        var startAncestor = start;

        while (startAncestor && startAncestor.parentNode != root)
            startAncestor = startAncestor.parentNode;

        if (!startAncestor)
            startAncestor = root;

        var endAncestor = end;
        while (endAncestor && endAncestor.parentNode != root)
            endAncestor = endAncestor.parentNode;

        if (!endAncestor)
            endAncestor = root;

        if (startAncestor == endAncestor)
            return 0;

        return findNodeIndex(endAncestor) - findNodeIndex(startAncestor);
    }

    function fixIvalidRange(range, toStart) {
        function isInvalidRange(range) {
            try {
                return compareBoundaries(range.startContainer, range.endContainer, range.startOffset, range.endOffset) < 0;
            } catch (ex) {
                // range was initially invalid (e.g. when cloned from invalid range) - it must be fixed
                return true;
            }
        }

        if (isInvalidRange(range)) {
            if (toStart) {
                range.commonAncestorContainer = range.endContainer = range.startContainer;
                range.endOffset = range.startOffset;
            } else {
                range.commonAncestorContainer = range.startContainer = range.endContainer;
                range.startOffset = range.endOffset;
            }

            range.collapsed = true;
        }
    }

    function updateRangeProperties(range) {
        range.collapsed = range.startContainer == range.endContainer && range.startOffset == range.endOffset;

        var node = range.startContainer;
        while (node && node != range.endContainer && !isAncestorOf(node, range.endContainer))
            node = node.parentNode;

        range.commonAncestorContainer = node;
    }

    function findNodeIndex(node) {
        var i = 0;
        while (node = node.previousSibling) i++;
        return i;
    }

    function isDataNode(node) {
        return node && node.nodeValue !== null && node.data !== null;
    }

    function isAncestorOf(parent, node) {
        try {
            return !isDataNode(parent) && ($.contains(parent, isDataNode(node) ? node.parentNode : node) || node.parentNode == parent);
        } catch (e) {
            return false;
        }
    }

    function isAncestorOrSelf(root, node) {
        return isAncestorOf(root, node) || root == node;
    }

    function findClosestAncestor(root, node) {
        if (isAncestorOf(root, node))
            while (node && node.parentNode != root)
                node = node.parentNode;

        return node;
    }

    function getNodeLength(node) {
        return isDataNode(node) ? node.length : node.childNodes.length;
    }

    function splitDataNode(node, offset) {
        var newNode = node.cloneNode(false);
        node.deleteData(offset, node.length);
        newNode.deleteData(0, offset);
        dom.insertAfter(newNode, node);
    }

    function createRange(document) {
        return document.createRange ? document.createRange() : new W3CRange(document);
    }

    function attrEquals(node, attributes) {
        for (var key in attributes) {
            var value = node[key];

            if (key == 'float')
                value = node[$.support.cssFloat ? "cssFloat" : "styleFloat"];

            if (typeof value == 'object') {
                if (!attrEquals(value, attributes[key]))
                    return false;
            } else if (value != attributes[key])
                return false;
        }

        return true;
    }

    function RangeIterator(range) {
        $.extend(this, {
            range: range,
            _current: null,
            _next: null,
            _end: null
        });

        if (range.collapsed)
            return;

        var root = range.commonAncestorContainer;

        this._next = range.startContainer == root && !isDataNode(range.startContainer) ?
        range.startContainer.childNodes[range.startOffset] :
        findClosestAncestor(root, range.startContainer);

        this._end = range.endContainer == root && !isDataNode(range.endContainer) ?
        range.endContainer.childNodes[range.endOffset] :
        findClosestAncestor(root, range.endContainer).nextSibling;
    }

    RangeIterator.prototype = {
        hasNext: function () {
            return !!this._next;
        },

        next: function () {
            var current = this._current = this._next;
            this._next = this._current && this._current.nextSibling != this._end ?
            this._current.nextSibling : null;

            if (isDataNode(this._current)) {
                if (this.range.endContainer == this._current)
                    (current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset);

                if (this.range.startContainer == this._current)
                    (current = current.cloneNode(true)).deleteData(0, this.range.startOffset);
            }

            return current;
        },

        traverse: function (callback) {
            function next() {
                this._current = this._next;
                this._next = this._current && this._current.nextSibling != this._end ? this._current.nextSibling : null;
                return this._current;
            }

            var current;

            while (current = next.call(this)) {
                if (this.hasPartialSubtree())
                    this.getSubtreeIterator().traverse(callback);
                else
                    callback(current)
            }

            return current;
        },

        remove: function (originalRange) {
            var inStartContainer = this.range.startContainer == this._current;
            var inEndContainer = this.range.endContainer == this._current;

            if (isDataNode(this._current) && (inStartContainer || inEndContainer)) {
                var start = inStartContainer ? this.range.startOffset : 0;
                var end = inEndContainer ? this.range.endOffset : this._current.length;
                var delta = end - start;

                if (originalRange && (inStartContainer || inEndContainer)) {
                    if (this._current == originalRange.startContainer && start <= originalRange.startOffset)
                        originalRange.startOffset -= delta;

                    if (this._current == originalRange.endContainer && end <= originalRange.endOffset)
                        originalRange.endOffset -= delta;
                }

                this._current.deleteData(start, delta);
            } else {
                var parent = this._current.parentNode;

                if (originalRange && (this.range.startContainer == parent || this.range.endContainer == parent)) {
                    var nodeIndex = findNodeIndex(this._current);

                    if (parent == originalRange.startContainer && nodeIndex <= originalRange.startOffset)
                        originalRange.startOffset -= 1;

                    if (parent == originalRange.endContainer && nodeIndex < originalRange.endOffset)
                        originalRange.endOffset -= 1;
                }

                dom.remove(this._current);
            }
        },

        hasPartialSubtree: function () {
            return !isDataNode(this._current) &&
            (isAncestorOrSelf(this._current, this.range.startContainer) ||
                isAncestorOrSelf(this._current, this.range.endContainer));
        },

        getSubtreeIterator: function () {
            var subRange = this.range.cloneRange();
            subRange.selectNodeContents(this._current);

            if (isAncestorOrSelf(this._current, this.range.startContainer))
                subRange.setStart(this.range.startContainer, this.range.startOffset);
            if (isAncestorOrSelf(this._current, this.range.endContainer))
                subRange.setEnd(this.range.endContainer, this.range.endOffset);

            return new RangeIterator(subRange);
        }
    };

    function W3CSelection(doc) {
        this.ownerDocument = doc;
        this.rangeCount = 1;
    };

    W3CSelection.prototype = {
        addRange: function (range) {
            var textRange = this.ownerDocument.body.createTextRange();

            // end container should be adopted first in order to prevent selection with negative length
            adoptContainer(textRange, range, false);
            adoptContainer(textRange, range, true);

            textRange.select();
        },

        removeAllRanges: function () {
            this.ownerDocument.selection.empty();
        },

        getRangeAt: function () {
            var textRange, range = new W3CRange(this.ownerDocument), selection = this.ownerDocument.selection;

            try {
                textRange = selection.createRange();
            } catch (ex) {
                return range;
            }

            if (selection.type == 'Control') {
                range.selectNode(textRange.item(0));
            } else {
                adoptEndPoint(textRange, range, true);
                adoptEndPoint(textRange, range, false);

                if (range.startContainer.nodeType == 9)
                    range.setStart(range.endContainer, range.startOffset);
                
                if (range.endContainer.nodeType == 9)
                    range.setEnd(range.startContainer, range.endOffset);

                if (textRange.compareEndPoints('StartToEnd', textRange) == 0)
                    range.collapse(false);
            }
            return range;
        }
    }

    function adoptContainer(textRange, range, start) {
        // find anchor node and offset
        var container = range[start ? 'startContainer' : 'endContainer'];
        var offset = range[start ? 'startOffset' : 'endOffset'], textOffset = 0;
        var anchorNode = isDataNode(container) ? container : container.childNodes[offset] || null;
        var anchorParent = isDataNode(container) ? container.parentNode : container;
        // visible data nodes need a text offset
        if (container.nodeType == 3 || container.nodeType == 4)
            textOffset = offset;

        // create a cursor element node to position range (since we can't select text nodes)
        var cursorNode = anchorParent.insertBefore(dom.create(range.ownerDocument, 'a'), anchorNode);

        var cursor = range.ownerDocument.body.createTextRange();
        cursor.moveToElementText(cursorNode);
        dom.remove(cursorNode);
        cursor[start ? 'moveStart' : 'moveEnd']('character', textOffset);
        cursor.collapse(false);
        textRange.setEndPoint(start ? 'StartToStart' : 'EndToStart', cursor);
    }

    function adoptEndPoint(textRange, range, start) {
        var cursorNode = dom.create(range.ownerDocument, 'a'), cursor = textRange.duplicate();
        cursor.collapse(start);
        var parent = cursor.parentElement();
        do {
            parent.insertBefore(cursorNode, cursorNode.previousSibling);
            cursor.moveToElementText(cursorNode);
        } while (cursor.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling);

        cursor.setEndPoint(start ? 'EndToStart' : 'EndToEnd', textRange);

        var target = cursorNode.nextSibling;

        if (!target) {
            // at end of text node
            target = cursorNode.previousSibling;

            if (target && isDataNode(target)) { // in case of collapsed range in empty tag
                range.setEnd(target, target.nodeValue.length);
                dom.remove(cursorNode);
            } else {
                range.selectNodeContents(parent);
                dom.remove(cursorNode);
                range.endOffset -= 1; // cursorNode was in parent
            }

            return;
        }

        dom.remove(cursorNode);

        if (isDataNode(target))
            range[start ? 'setStart' : 'setEnd'](target, cursor.text.length);
        else
            range[start ? 'setStartBefore' : 'setEndBefore'](target);
    }

    var $t = $.telerik;

    function createContentElement($textarea) {
        $editableArea = $textarea.parent();
        var marginPaddingAndBorder = $editableArea.outerHeight(true) - $editableArea.height();
        $editableArea.height($editableArea.parent().height() - $editableArea.prev().outerHeight(true) - marginPaddingAndBorder);

        var iframe = $('<iframe />', {
                         src: 'javascript:"<html></html>"',
                         frameBorder: '0'
                     })
                     .css({
                        width: '100%',
                        height: '100%'
                     })
                     .insertBefore($textarea)[0];

        var window = iframe.contentWindow || iframe;
        var document = window.document || iframe.contentDocument;

        document.designMode = 'On';
        document.open();
        document.write(
            '<!DOCTYPE><html><head xmlns="http://www.w3.org/1999/xhtml">' +
            '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
            '<style type="text/css">' +
                'html,body{padding:0px;margin:0;font:12px/1.4em Verdana,Geneva,sans-serif;background:#fff;}' + 
                'p{margin:.7em 0;}p:first-child{margin-top:0;}.t-marker{display:none;}' + 
                'a{color:#00a}' +
            '</style>' +
            '</head><body spellcheck="false">' +
            $textarea.val() +
            '</body></html>');
        
        document.close();

        return window;
    };

    $t.editor = function (element, options) {
        /* suppress initialization in mobile webkit devices (w/o proper contenteditable support) */
        if (/Mobile.*Safari/.test(navigator.userAgent))
            return;

        var self = this;

        this.element = element;

        var $element = $(element);

        if ($element.is('textarea'))
            $t.editor.extend(element);

        $element.closest('form').bind('submit', function () {
            self.update();
        });

        $.extend(this, options);

        $t.bind(this, {
            load: this.onLoad,
            selectionChange: this.onSelectionChange,
            change: this.onChange,
            execute: this.onExecute
        });

        this.textarea = $element.find('textarea')[0];

        this.window = createContentElement($(this.textarea));

        this.document = this.window.contentDocument || this.window.document;
        this.body = this.document.body;
        this.keyboard = new Keyboard([new TypingHandler(this), new SystemHandler(this)]);

        this.pendingFormats = [];
        
        var buttons = '.t-editor-button .t-icon';

        this.undoRedoStack = new UndoRedoStack();

        function toolFromClassName(element) {
            return $.grep(element.className.split(' '), function (x) {
                return !/^t-(icon|state-hover)$/i.test(x);
            })[0].substring(2).toLowerCase();
        }

        function descriptionText(tool) {
            if (!tool.key)
                return tool.description;

            return new $t.stringBuilder()
                .cat(tool.description)
                .cat(' (').catIf('Ctrl + ', tool.ctrl).catIf('Shift + ', tool.shift).catIf('Alt + ', tool.alt).cat(tool.key).cat(')')
                .string();
        }

        $(this.window).bind('blur', function () {
            var old = self.textarea.value;
            var value = self.encodedValue();
            self.update(value);
            if (value != old)
                $t.trigger(self.element, 'change')
        });

        $element
            .delegate(buttons, 'mouseenter', $t.hover)
            .delegate(buttons, 'mouseleave', $t.leave)
            .delegate(buttons, 'mousedown', $t.preventDefault)
            .delegate(buttons, 'click', $t.stopAll(function (e) {
                self.focus();
                self.exec(toolFromClassName(this));
            }))
            .bind('selectionChange', function() {
                var range = self.getRange();
                var nodes = textNodes(range);
                if (!nodes.length)
                    nodes = [range.startContainer];

                $element.find(buttons).each(function () {
                    var tool = self.tools[toolFromClassName(this)];
                    $(this).toggleClass('t-state-active', tool.finder().isFormatted(nodes));
                });
            })
            .find(buttons)
                .each(function () {
                    var tool = self.tools[toolFromClassName(this)];
                    $(this).attr('unselectable', 'on')
                           .attr('title', descriptionText(tool));
                });

        $(this.document)
            .bind('contextmenu', $t.preventDefault)
            .bind('keyup', function (e) {
                var selectionCodes = [8, 9, 13, 33, 34, 35, 36, 37, 38, 39, 40, 40, 45, 46];

                if ($.inArray(e.keyCode, selectionCodes) > -1)
                    selectionChanged(self);
            })
            .keydown(function (e) {
                var toolName = self.keyboard.toolFromShortcut(self.tools, e);
                if (toolName) {
                    e.preventDefault();
                    self.exec(toolName);
                    return false;
                }

                self.keyboard.clearTimeout();

                self.keyboard.keydown(e);
            })
            .keyup(function (e) {
                self.keyboard.keyup(e);
            })
            .mouseup(function () {
                selectionChanged(self);
            });
    };

    function selectionChanged(editor) {
        $t.trigger(editor.element, 'selectionChange');
    }

    function documentFromRange(range) {
        var startContainer = range.startContainer;
        return startContainer.nodeType == 9 ? startContainer : startContainer.ownerDocument;
    }

    function selectionFromWindow(window) {
        return window.getSelection ? window.getSelection() : new W3CSelection(window.document);
    }

    function selectionFromRange(range) {
        var document = documentFromRange(range);
        return selectionFromDocument(document);
    }

    function selectionFromDocument(document) {
        return selectionFromWindow(windowFromDocument(document));
    }

    function windowFromDocument(document) {
        return document.defaultView || document.parentWindow;
    }

    function selectRange(range) {
        var image = RangeUtils.image(range);
        if (image) {
            range.setStartAfter(image);
            range.setEndAfter(image);
        }
        var selection = selectionFromRange(range);
        selection.removeAllRanges();
        selection.addRange(range);
    }

    function normalize(node) {
        if (node.nodeType == 1)
            node.normalize();
    }

    function domToXhtml(root) {
        var rgb = /rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i;
        var fontSizeMappings = 'xx-small,x-small,small,medium,large,x-large,xx-large'.split(',');

        function toHex(color) {
            var matches = rgb.exec(color);

            if (!matches) return color;

            return '#' + $.map(matches.slice(1), function (x) {
                return x = parseInt(x).toString(16), x.length > 1 ? x : '0' + x;
            }).join('');
        }

        var result = [];
        var tagMap = {
            'telerik:script': { start: function (node) { result.push('<script'); attr(node); result.push('>'); }, end: function () { result.push('</script>') } },
            b: { start: function () { result.push('<strong>') }, end: function () { result.push('</strong>') } },
            i: { start: function () { result.push('<em>') }, end: function () { result.push('</em>') } },
            u: { start: function () { result.push('<span style="text-decoration:underline;">') }, end: function () { result.push('</span>') } },
            font: {
                start: function (node) {
                    result.push('<span style="');

                    var color = node.getAttribute('color');
                    var size = fontSizeMappings[node.getAttribute('size')];
                    var face = node.getAttribute('face');

                    if (color) {
                        result.push('color:')
                        result.push(toHex(color));
                        result.push(';');
                    }

                    if (face) {
                        result.push('font-face:');
                        result.push(face);
                        result.push(';');
                    }

                    if (size) {
                        result.push('font-size:');
                        result.push(size);
                        result.push(';');
                    }

                    result.push('">');
                },
                end: function (node) {
                    result.push('</span>');
                }
            }
        };

        function attr(node) {
            var specifiedAttributes = [];
            var attributes = node.attributes;

            for (var i = 0, l = attributes.length; i < l; i++) {
                var attribute = attributes[i];
                var name = attribute.nodeName;
                // In IE < 8 the 'value' attribute is not returned as 'specified'. The same goes for type="text"
                if (attribute.specified || (name == 'value' && node.value != '') || (name == 'type' && attribute.nodeValue == 'text'))
                    if (name.indexOf('_moz') < 0 && name != 'complete')
                        specifiedAttributes.push(attribute);
            }

            if (!specifiedAttributes.length)
                return;

            specifiedAttributes.sort(function (a, b) {
                return a.nodeName > b.nodeName ? 1 : a.nodeName < b.nodeName ? -1 : 0;
            });

            for (var i = 0, l = specifiedAttributes.length; i < l; i++) {
                var attribute = specifiedAttributes[i];
                var attributeName = attribute.nodeName;
                var attributeValue = attribute.nodeValue;

                result.push(' ');
                result.push(attributeName);
                result.push('="');
                if (attributeName == 'style') {
                    // In IE < 8 the style attribute does not return proper nodeValue
                    var css = $.trim(attributeValue || node.style.cssText);
                    $.each(css.split(';'), function () {
                        if (this.length) {
                            var propertyAndValue = this.split(':');
                            var property = $.trim(propertyAndValue[0].toLowerCase()),
                                value = $.trim(propertyAndValue[1]);

                            if (property.indexOf('color') >= 0)
                                value = toHex(value);

                            result.push(property);
                            result.push(':');
                            result.push(value);
                            result.push(';');
                        }
                    });
                } else if (attributeName == 'src' || attributeName == 'href') {
                    result.push(node.getAttribute(attributeName, 2));
                } else {
                    result.push(fillAttrs[attributeName] ? attributeName : attributeValue);
                }

                result.push('"');
            }
        }

        function children(node, skip) {
            for (var childNode = node.firstChild; childNode; childNode = childNode.nextSibling)
                child(childNode, skip);
        }

        function child(node, skip) {
            var nodeType = node.nodeType;
            if (nodeType == 1) {
                if (node.attributes['_moz_dirty'] && dom.is(node, 'br'))
                    return;

                var tagName = dom.name(node);
                var mapper = tagMap[tagName];

                if (mapper) {
                    mapper.start(node);
                    children(node);
                    mapper.end(node);
                    return;
                }

                result.push('<');
                result.push(tagName);

                attr(node);

                if (empty[tagName]) {
                    result.push(' />');
                } else {
                    result.push('>');
                    children(node, skip || dom.is(node, 'pre'));
                    result.push('</');
                    result.push(tagName);
                    result.push('>');
                }
            } else if (nodeType == 3) {
                var value = node.nodeValue;
                
                if (!skip && $.support.leadingWhitespace) {
                    var previous = node.previousSibling;
                    
                    if (!previous || previous.innerHTML == '' || dom.isBlock(previous))
                        value = value.replace(/^[\r\n\v\f\t ]+/, '');
                    
                    value = value.replace(/ +/, ' ');
                }
                
                result.push(dom.encode(value));
            
            } else if (nodeType == 4) {
                result.push('<![CDATA[');
                result.push(node.data);
                result.push(']]>');
            } else if (nodeType == 8) {
                if (node.data.indexOf('[CDATA[') < 0) {
                    result.push('<!--');
                    result.push(node.data);
                    result.push('-->');
                } else {
                    result.push('<!');
                    result.push(node.data);
                    result.push('>');
                }
            }
        }

        children(root);

        return result.join('');
    }

    function split(range, node, trim) {
        function partition(start) {
            var partitionRange = range.cloneRange();
            partitionRange.collapse(start);
            partitionRange[start ? 'setStartBefore' : 'setEndAfter'](node);
            var contents = partitionRange.extractContents();
            if (trim)
                contents = dom.trim(contents);
            dom[start ? 'insertBefore' : 'insertAfter'](contents, node);
        }
        partition(true);
        partition(false);
    }

    var whitespace = /^\s+$/;

    var dom = {
        blockParentOrBody: function(node) {
            return dom.parentOfType(node, blockElements) || node.ownerDocument.body;
        },

        encode: function (value) {
            return value.replace(/&/g, '&amp;')
                    .replace(/</g, '&lt;')
                    .replace(/>/g, '&gt;')
                    .replace(/\u00a0/g, '&nbsp;');
        },
        name: function (node) {
            return node.nodeName.toLowerCase();
        },
        
        significantChildNodes: function(node) {
            return $.grep(node.childNodes, function(child) {
                return child.nodeType != 3 || !dom.isWhitespace(child);
            });
        },

        is: function (node, nodeName) {
            return dom.name(node) == nodeName;
        },
        
        isMarker: function(node) {
            return node.className == 't-marker';
        },
        
        isWhitespace: function(node) {
            return whitespace.test(node.nodeValue);
        },
        
        isBlock: function(node) {
            return block[dom.name(node)];
        },

        isEmpty: function(node) {
            return empty[dom.name(node)];
        },
        
        isInline: function(node) {
            return inline[dom.name(node)];
        },
        
        scrollTo: function (node) {
            node.ownerDocument.body.scrollTop = $(isDataNode(node) ? node.parentNode : node).offset().top;
        },

        insertAt: function (parent, newElement, position) {
            parent.insertBefore(newElement, parent.childNodes[position] || null);
        },

        insertBefore: function (newElement, referenceElement) {
            return referenceElement.parentNode.insertBefore(newElement, referenceElement);
        },

        insertAfter: function (newElement, referenceElement) {
            return referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
        },

        remove: function (node) {
            node.parentNode.removeChild(node);
        },

        trim: function (parent) {
            for (var i = parent.childNodes.length - 1; i >= 0; i--) {
                var node = parent.childNodes[i];
                if (isDataNode(node)) {
                    if (node.nodeValue.replace(/\ufeff/g, '').length == 0)
                        dom.remove(node);
                } else if (node.className != 't-marker') {
                    dom.trim(node);
                    if (node.childNodes.length == 0)
                        dom.remove(node);
                }
            }

            return parent;
        },

        parentOfType: function (node, tags) {
            do {
                node = node.parentNode;
            } while (node && !(dom.ofType(node, tags)));

            return node;
        },

        ofType: function (node, tags) {
            return $.inArray(dom.name(node), tags) >= 0;
        },

        changeTag: function (referenceElement, tagName) {
            var newElement = dom.create(referenceElement.ownerDocument, tagName);
            var attributes = referenceElement.attributes;

            for (var i = 0; i < attributes.length; i++) {
                var attribute = attributes[i];
                if (attribute.specified) {
                    // IE < 8 cannot set class or style via setAttribute
                    var name = attribute.nodeName;
                    var value = attribute.nodeValue;
                    if (name == 'class')
                        newElement.className = value;
                    else if (name == 'style')
                        newElement.style.cssText = referenceElement.style.cssText;
                    else
                        newElement.setAttribute(name, value);
                }
            }

            while (referenceElement.firstChild)
                newElement.appendChild(referenceElement.firstChild);

            dom.insertBefore(newElement, referenceElement);
            dom.remove(referenceElement);
            return newElement;
        },

        wrap: function (node, wrapper) {
            dom.insertBefore(wrapper, node);
            wrapper.appendChild(node);
            return wrapper;
        },

        unwrap: function (node) {
            var parent = node.parentNode;

            while (node.firstChild)
                parent.insertBefore(node.firstChild, node);

            parent.removeChild(node);
        },

        create: function (document, tagName, attributes) {
            return dom.attr(document.createElement(tagName), attributes);
        },

        attr: function (element, attributes) {
            attributes = $.extend({}, attributes);

            if (attributes && 'style' in attributes) {
                dom.style(element, attributes.style);
                delete attributes.style;
            }
            return $.extend(element, attributes);
        },

        style: function (node, value) {
            $(node).css(value || {});
        },

        unstyle: function (node, value) {
            for (var key in value) {
                if (key == 'float')
                    key = $.support.cssFloat ? "cssFloat" : "styleFloat";

                node.style[key] = '';
            }

            if (node.style.cssText == '')
                node.removeAttribute('style');
        },

        commonAncestor: function () {
            var count = arguments.length;

            if (!count)
                return null;

            if (count == 1)
                return arguments[0];

            var paths = [];
            var minPathLength = Infinity;

            for (var i = 0; i < count; i++) {
                var ancestors = [];
                var node = arguments[i];
                while (node) {
                    ancestors.push(node);
                    node = node.parentNode;
                }
                paths.push(ancestors.reverse());
                minPathLength = Math.min(minPathLength, ancestors.length);
            }

            if (count == 1)
                return paths[0][0];

            var output = null;
            for (i = 0; i < minPathLength; i++) {
                var first = paths[0][i];

                for (var j = 1; j < count; j++)
                    if (first != paths[j][i])
                        return output;

                output = first;
            }
            return output;
        }
    }

    function RangeEnumerator(range) {
        this.enumerate = function () {
            var nodes = [];

            function visit(node) {
                if (dom.is(node, 'img') || (node.nodeType == 3 && !dom.isWhitespace(node))) {
                    nodes.push(node);
                } else {
                    node = node.firstChild;
                    while (node) {
                        visit(node);
                        node = node.nextSibling;
                    }
                }
            }

            new RangeIterator(range).traverse(visit);

            return nodes;
        }
    }

    function textNodes(range) {
        return new RangeEnumerator(range).enumerate();
    }

    function getMarkers(range) {
        var markers = [];

        new RangeIterator(range).traverse(function (node) {
            if (node.className == 't-marker')
                markers.push(node);
        });

        return markers;
    }

    function LinkFormatter() {
        this.finder = new LinkFormatFinder();

        this.apply = function (range, attributes) {
            var nodes = textNodes(range);
            if (attributes.innerHTML != undefined) {
                var markers = getMarkers(range);
                var document = documentFromRange(range);
                range.deleteContents();
                var a = dom.create(document, 'a', attributes);
                range.insertNode(a);

                if (markers.length > 1) {
                    dom.insertAfter(markers[markers.length - 1], a);
                    dom.insertAfter(markers[1], a);
                    dom[nodes.length > 0 ? 'insertBefore' : 'insertAfter'](markers[0], a);
                }
            } else {
                var formatter = new InlineFormatter([{ tags: ['a']}], attributes);
                formatter.finder = this.finder;
                formatter.apply(nodes);
            }
        }
    }

    function InlineFormatter(format, values) {
        this.finder = new InlineFormatFinder(format);

        var attributes = $.extend({}, format[0].attr, values);

        var tag = format[0].tags[0];

        function wrap(node) {
            return dom.wrap(node, dom.create(node.ownerDocument, tag, attributes));
        }

        this.toggle = function (range) {
            var nodes = textNodes(range);
            var isPending = nodes.length == 0;
            var caretMarker;
            
            if (isPending) {
                var markers = getMarkers(range);
                caretMarker = markers[1];
                var invisibleNode = this.editor.document.createTextNode('\ufeff');
                dom.insertBefore(invisibleNode, caretMarker);
                nodes.push(invisibleNode);
                invisibleNode = invisibleNode.cloneNode(true);
                dom.insertAfter(invisibleNode, caretMarker);
                nodes.push(invisibleNode);
                range.setEndAfter(markers[2]);
            }
            
            if (this.finder.isFormatted(nodes)) {
                this.split(range);
                this.remove(nodes);
            } else
                this.apply(nodes);

            if (isPending)
                this.editor.pendingFormats.push(caretMarker.parentNode);
        }

        this.apply = function (nodes) {
            var formatNodes = [];
            for (var i = 0, l = nodes.length; i < l; i++) {
                var node = nodes[i];

                var formatNode = this.finder.findSuitable(node);
                if (formatNode)
                    dom.attr(formatNode, attributes);
                else
                    formatNode = wrap(node);

                formatNodes.push(formatNode);
            }

            this.consolidate(formatNodes);
        }

        this.remove = function (nodes) {
            for (var i = 0, l = nodes.length; i < l; i++) {
                var formatNode = this.finder.findFormat(nodes[i]);
                if (formatNode)
                    dom.unwrap(formatNode);
            }
        }

        this.split = function (range) {
            var nodes = textNodes(range);

            var isPending = nodes.length == 0;
            var caretMarker;
            
            if (isPending) {
                caretMarker = getMarkers(range)[1];
                nodes.push(caretMarker);
            }

            for (var i = 0, l = nodes.length; i < l; i++) {
                var formatNode = this.finder.findFormat(nodes[i]);
                if (formatNode)
                    split(range, formatNode, true);
            }
        }

        this.consolidate = function (nodes) {
            while (nodes.length > 1) {
                var node = nodes.pop();
                var last = nodes[nodes.length - 1];

                if (node.previousSibling && node.previousSibling.className == 't-marker') {
                    last.appendChild(node.previousSibling);
                }

                if (node.previousSibling == last) {
                    while (node.firstChild)
                        last.appendChild(node.firstChild);
                    dom.remove(node);
                }
            }
        }
    }

    function ListFormatter(tag) {
        var finder = new ListFormatFinder(tag);

        function wrap(list, nodes) {
            var li = dom.create(list.ownerDocument, 'li');

            for (var i = 0; i < nodes.length; i++) {
                var node = nodes[i];

                if (dom.is(node, 'li')) {
                    list.appendChild(node);
                    continue;
                }

                li.appendChild(node);

                if (dom.isBlock(node)) {
                    list.appendChild(li);
                    li = li.cloneNode(false);
                }
            }

            if (li.childNodes.length)
                list.appendChild(li);
        }

        function containsAny(parent, nodes) {
            for (var i = 0; i < nodes.length; i++)
                if (isAncestorOrSelf(parent, nodes[i]))
                    return true;

            return false;
        }

        function suitable(candidate, nodes) {
            return containsAny(candidate, nodes) || dom.isInline(candidate) || candidate.nodeType == 3;
        }

        this.split = function (range) {
            var nodes = textNodes(range);
            if (nodes.length) {
                var start = dom.parentOfType(nodes[0], ['li']);
                var end = dom.parentOfType(nodes[nodes.length - 1], ['li'])
                range.setStartBefore(start);
                range.setEndAfter(end);

                for (var i = 0, l = nodes.length; i < l; i++) {
                    var formatNode = finder.findFormat(nodes[i]);
                    if (formatNode)
                        split(range, formatNode, true);
                }
            }
        }

        this.apply = function (nodes) {
            var commonAncestor = nodes.length == 1 ? nodes[0].ownerDocument.body : dom.commonAncestor.apply(null, nodes);
            
            if (dom.isInline(commonAncestor))
                commonAncestor = dom.blockParentOrBody(commonAncestor);

            var ancestors = [];

            var formatNode = finder.findSuitable(nodes);

            if (!formatNode)
                formatNode = new ListFormatFinder(tag == 'ul' ? 'ol' : 'ul').findSuitable(nodes);
            
            var childNodes = dom.significantChildNodes(commonAncestor);

            for (var i = 0; i < childNodes.length; i++) {
                var child = childNodes[i];
                var nodeName = dom.name(child);
                if (suitable(child, nodes) && (!formatNode || !isAncestorOrSelf(formatNode, child))) {

                    if (formatNode && (nodeName == 'ul' || nodeName == 'ol')) {
                        // merging lists
                        $.each(child.childNodes, function () { ancestors.push(this) });
                        dom.remove(child);
                    } else {
                        ancestors.push(child);
                    }
                }
            }

            if (ancestors.length == childNodes.length && commonAncestor != nodes[0].ownerDocument.body)
                ancestors = [commonAncestor];

            if (!formatNode) {
                formatNode = dom.create(commonAncestor.ownerDocument, tag);
                dom.insertBefore(formatNode, ancestors[0]);
            }

            wrap(formatNode, ancestors);

            if (!dom.is(formatNode, tag))
                dom.changeTag(formatNode, tag);
        }

        function unwrap(ul) {
            for (var li = ul.firstChild; li; li = li.nextSibling) {
                var p = dom.create(ul.ownerDocument, 'p');
                
                while(li.firstChild) {
                    var child = li.firstChild;
                    if (dom.isBlock(child))
                        dom.insertBefore(child, ul);
                    else
                        p.appendChild(child);
                }
                if (p.firstChild)
                    dom.insertBefore(p, ul);
            }
            
            dom.remove(ul);
        }

        this.remove = function (nodes) {
            var formatNode;
            for (var i = 0, l = nodes.length; i < l; i++)
                if (formatNode = finder.findFormat(nodes[i]))
                    unwrap(formatNode);
        }

        this.toggle = function (range) {
            var nodes = textNodes(range);
            if (!nodes.length) {
                range.selectNodeContents(range.commonAncestorContainer);
                nodes = textNodes(range);
                if (!nodes.length)
                    nodes = dom.significantChildNodes(range.commonAncestorContainer);
            }
            
            if (finder.isFormatted(nodes)) {
                this.split(range);
                this.remove(nodes);
            } else
                this.apply(nodes);
        }
    }
    function BlockFormatter(format, values) {
        var finder = new BlockFormatFinder(format);

        function wrap(tag, attributes, nodes) {
            var commonAncestor = nodes.length == 1 ? dom.blockParentOrBody(nodes[0]) : dom.commonAncestor.apply(null, nodes);

            if (dom.isInline(commonAncestor))
                commonAncestor = dom.blockParentOrBody(commonAncestor);
            
            var ancestors = dom.significantChildNodes(commonAncestor);

            var position = findNodeIndex(ancestors[0]);

            var wrapper = dom.create(commonAncestor.ownerDocument, tag, attributes);
            
            for (var i = 0; i < ancestors.length; i++) {
                var ancestor = ancestors[i];
                if (dom.isBlock(ancestor)){
                    dom.attr(ancestor, attributes);

                    if (wrapper.childNodes.length) {
                        dom.insertBefore(wrapper, ancestor);
                        wrapper = wrapper.cloneNode(false);
                    }

                    position = findNodeIndex(ancestor) + 1;

                    continue;
                }
                
                wrapper.appendChild(ancestor);
            }

            if (wrapper.firstChild)
                dom.insertAt(commonAncestor, wrapper, position);
        }

        this.apply = function (nodes) {
            var formatNode = dom.is(nodes[0], 'img') ? nodes[0] : finder.findSuitable(nodes);

            var formatToApply = formatNode ? formatByName(dom.name(formatNode), format) : format[0];

            var tag = formatToApply.tags[0];
            var attributes = $.extend({}, formatToApply.attr, values);

            if (formatNode)
                dom.attr(formatNode, attributes);
            else
                wrap(tag, attributes, nodes);
        }

        this.remove = function (nodes) {
            for (var i = 0, l = nodes.length; i < l; i++) {
                var formatNode = finder.findFormat(nodes[i]);
                if (formatNode)
                    if (dom.ofType(formatNode, ['p', 'img']))
                        dom.unstyle(formatNode, formatByName(dom.name(formatNode), format).attr.style);
                    else
                        dom.unwrap(formatNode);
            }
        }

        this.toggle = function (range) {
            var nodes = textNodes(range);
            if (!nodes.length) {
                range.selectNodeContents(range.commonAncestorContainer);
                nodes = textNodes(range);
                if (!nodes.length)
                    nodes = dom.significantChildNodes(range.commonAncestorContainer);
            }
            if (finder.isFormatted(nodes))
                this.remove(nodes);
            else
                this.apply(nodes);
        }
    }

    function ListFormatFinder(tag) {
        BlockFormatFinder.call(this, [{ tags: [tag]}]);

        this.isFormatted = function (nodes) {
            var formatNodes = [];
            for (var i = 0; i < nodes.length; i++) {
                var formatNode = this.findFormat(nodes[i]);
                if (formatNode && $.inArray(formatNode, formatNodes) < 0)
                    formatNodes.push(formatNode);
            }

            return formatNodes.length == 1;
        }

        this.findSuitable = function (nodes) {
            return dom.parentOfType(nodes[0], [tag]);
        }
    }

    function BlockFormatFinder(format) {
        function contains(node, children) {
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                if (child == null || !isAncestorOrSelf(node, child))
                    return false;
            }

            return true;
        }

        this.findSuitable = function (nodes) {
            var suitable = [];

            for (var i = 0; i < nodes.length; i++)
                suitable.push(dom.parentOfType(nodes[i], format[0].tags));

            for (var i = 0; i < suitable.length; i++)
                if (contains(suitable[i], suitable))
                    return suitable[i];

            return null;
        }

        this.findFormat = function (sourceNode) {
            for (var i = 0; i < format.length; i++) {
                var node = sourceNode;
                var tags = format[i].tags;
                var attributes = format[i].attr;

                while (node) {
                    if (dom.ofType(node, tags) && attrEquals(node, attributes))
                        return node;
                    node = node.parentNode;
                }
            }
            return null;
        },

        this.isFormatted = function (nodes) {
            for (var i = 0; i < nodes.length; i++)
                if (!this.findFormat(nodes[i]))
                    return false;

            return true;
        }
    }

    function LinkFormatFinder() {
        this.findSuitable = function (sourceNode, skip) {
            return dom.parentOfType(sourceNode, ['a']);
        }
    }

    function InlineFormatFinder(format) {
        function numberOfSiblings(referenceNode) {
            var count = 0;

            for (var node = referenceNode.parentNode.firstChild; node; node = node.nextSibling)
                if (node != referenceNode && node.className != 't-marker' && node.nodeType == 1)
                    count++;

            return count;
        }

        this.findSuitable = function (sourceNode, skip) {
            if (!skip && numberOfSiblings(sourceNode) > 0)
                return null;

            return dom.parentOfType(sourceNode, format[0].tags);
        }

        this.findFormat = function (sourceNode) {
            for (var i = 0; i < format.length; i++) {
                var node = sourceNode;
                var tags = format[i].tags;
                var attributes = format[i].attr;

                if (node && dom.ofType(node, tags) && attrEquals(node, attributes))
                    return node;

                while (node) {
                    node = dom.parentOfType(node, tags);
                    if (node && attrEquals(node, attributes))
                        return node;
                }
            }
            return null;
        }

        this.isFormatted = function (nodes) {
            for (var i = 0; i < nodes.length; i++)
                if (this.findFormat(nodes[i]))
                    return true;

            return false;
        }
    }

    function RestorePoint(range) {
        var rootNode = documentFromRange(range);

        this.body = rootNode.body;

        this.html = this.body.innerHTML;

        function nodeToPath(node) {
            var path = [];

            while (node != rootNode) {
                path.push(findNodeIndex(node));
                node = node.parentNode;
            }

            return path;
        }

        function pathToNode(path) {
            var node = rootNode, length = path.length;

            while (length--)
                node = node.childNodes[path[length]];

            return node;
        }

        this.startContainer = nodeToPath(range.startContainer);
        this.endContainer = nodeToPath(range.endContainer);
        this.startOffset = range.startOffset;
        this.endOffset = range.endOffset;

        this.toRange = function () {
            var result = range.cloneRange();

            result.setStart(pathToNode(this.startContainer), this.startOffset);
            result.setEnd(pathToNode(this.endContainer), this.endOffset);

            return result;
        }
    }

    function Command(range) {
        var restorePoint = new RestorePoint(range);
        var marker = new Marker();

        this.getRange = function () {
            return restorePoint.toRange();
        }

        this.lockRange = function (expand) {
            return marker.add(this.getRange(), expand);
        }

        this.releaseRange = function (range) {
            marker.remove(range);
            selectRange(range);
        }

        this.undo = function () {
            restorePoint.body.innerHTML = restorePoint.html;
            selectRange(restorePoint.toRange());
        }

        this.redo = function () {
            this.exec();
        }
    }

    function ParagraphCommand(options) {
        Command.call(this, options.range);

        this.exec = function () {
            var range = this.getRange();

            var document = documentFromRange(range);

            var emptyParagraphContent = $.browser.msie ? '' : '<br _moz_dirty="" />';

            // necessary while the emptyParagraphContent is empty under IE
            var startInBlock = dom.parentOfType(range.startContainer, ['p']),
                endInBlock = dom.parentOfType(range.endContainer, ['p']),
                shouldTrim = (startInBlock && !endInBlock) || (!startInBlock && endInBlock);

            range.deleteContents();

            var marker = dom.create(document, 'a');
            range.insertNode(marker);

            var li = dom.parentOfType(marker, 'li'.split(','));

            var next;

            if (li) {
                var rng = range.cloneRange();
                rng.selectNode(li);

                if (textNodes(rng).length == 0) {
                    // hitting 'enter' in empty li
                    var paragraph = dom.create(document, 'p');

                    if (li.nextSibling)
                        split(rng, li.parentNode);

                    dom.insertAfter(paragraph, li.parentNode);
                    dom.remove(li.parentNode.childNodes.length == 1 ? li.parentNode : li);
                    paragraph.innerHTML = emptyParagraphContent;
                    next = paragraph;
                }
            }

            if (!next) {
                if (!li)
                    new BlockFormatter([{ tags: ['p']}]).apply([marker]);

                range.selectNode(marker);

                var parent = dom.parentOfType(marker, [li ? 'li' : 'p']);

                split(range, parent, shouldTrim);

                var previous = parent.previousSibling;

                if (dom.is(previous, 'li') && previous.firstChild && !dom.is(previous.firstChild, 'br'))
                    previous = previous.firstChild;

                next = parent.nextSibling;

                if (dom.is(next, 'li') && next.firstChild && !dom.is(next.firstChild, 'br'))
                    next = next.firstChild;

                dom.remove(parent);

                if (previous.firstChild && dom.is(previous.firstChild, 'br'))
                    dom.remove(previous.firstChild);

                if (isDataNode(previous) && previous.nodeValue == '')
                    previous = previous.parentNode;

                if (previous && previous.innerHTML == '')
                    previous.innerHTML = emptyParagraphContent;

                if (next.firstChild && dom.is(next.firstChild, 'br'))
                    dom.remove(next.firstChild);

                if (isDataNode(next) && next.nodeValue == '')
                    next = next.parentNode;

                if (next.innerHTML == '')
                    next.innerHTML = emptyParagraphContent;

                // normalize updates the caret display in Gecko
                normalize(previous);
            }

            normalize(next);

            range.selectNodeContents(next);
            range.collapse(true);

            dom.scrollTo(next);

            selectRange(range);
        }
    }

    function NewLineCommand(options) {
        Command.call(this, options.range);

        this.exec = function () {
            var range = this.getRange();
            range.deleteContents();
            var br = dom.create(documentFromRange(range), 'br');
            range.insertNode(br);
            br.parentNode.normalize();
            if (!$.browser.msie && (!br.nextSibling || dom.isWhitespace(br.nextSibling))) { 
                //Gecko and WebKit cannot put the caret after only one br.
                var filler = br.cloneNode(true);
                filler.setAttribute('_moz_dirty', '');
                dom.insertAfter(filler, br);
            }
            range.setStartAfter(br);
            range.collapse(true);
            selectRange(range);
        }
    }

    function ListCommand(options) {
        Command.call(this, options.range);
        var formatter = new ListFormatter(options.tag);

        this.exec = function () {
            var range = this.lockRange(true);
            
            formatter.toggle(range);

            this.releaseRange(range);
        }
    }

    function UnlinkCommand(options) {
        Command.call(this, options.range);

        this.exec = function () {
            var range = this.lockRange(true);

            var formatter = new InlineFormatter([{ tags: ['a']}]);
            formatter.remove(textNodes(range));

            this.releaseRange(range);
        }
    }

    function ImageCommand(options) {
        Command.call(this, options.range);
        this.async = true;
        var attributes;

        function insertImage(img, range) {
            if (attributes.src && attributes.src != 'http://') {
                if (!img) {
                    img = dom.create(documentFromRange(range), 'img', attributes);
                    img.onload = img.onerror = function() {
                        img.removeAttribute('complete');
                        img.removeAttribute('width');
                        img.removeAttribute('height');
                    }
                    range.deleteContents();
                    range.insertNode(img);
                    range.setStartAfter(img);
                    range.setEndAfter(img);
                    selectRange(range);
                    return true;
                } else
                    dom.attr(img, attributes);
            }

            return false;
        }

        this.redo = function () {
            var range = this.lockRange();
            if (!insertImage(RangeUtils.image(range), range))
                this.releaseRange(range);
        }

        this.exec = function () {
            var range = this.lockRange();

            var applied = false;

            var img = RangeUtils.image(range);

            function apply(e) {
                attributes = {
                    src: $('#t-editor-image-url', dialog.element).val(),
                    alt: $('#t-editor-image-title', dialog.element).val()
                };

                applied = insertImage(img, range);

                close(e);

                if (self.change)
                    self.change();
            }

            var self = this;

            function close(e) {
                e.preventDefault();
                dialog.destroy();

                windowFromDocument(documentFromRange(range)).focus();
                if (!applied)
                    self.releaseRange(range);
            }

            var dialog = $t.window.create({
                title: "Insert image",
                html: new $.telerik.stringBuilder()
                        .cat('<div class="t-editor-dialog">')
                            .cat('<ol>')
                                .cat('<li class="t-form-text-row"><label for="t-editor-image-url">Web address</label><input type="text" id="t-editor-image-url"/></li>')
                                .cat('<li class="t-form-text-row"><label for="t-editor-image-title">Tooltip</label><input type="text" id="t-editor-image-title"/></li>')
                            .cat('</ol>')
                            .cat('<div class="t-button-wrapper">')
                                .cat('<button class="t-dialog-insert t-button t-state-default">Insert</button>')
                                .cat('&nbsp;or&nbsp;')
                                .cat('<a href="#" class="t-dialog-close">Close</a>')
                            .cat('</div>')
                        .cat('</div>')
                        .string(),
                modal: true, resizable: false, draggable: true,
                onClose: close
            })
                .css('display', 'none')
                .find('.t-dialog-insert').click(apply).end()
                .find('.t-dialog-close').click(close).end()
                .find('.t-form-text-row input').keydown(function (e) {
                    if (e.keyCode == 13)
                        apply(e);
                    else if (e.keyCode == 27)
                        close(e);
                }).end()
                .delegate('.t-button', 'mouseenter', $t.buttonHover)
                .delegate('.t-button', 'mouseleave', $t.buttonLeave)
            // IE < 8 returns absolute url if getAttribute is not used
                .find('#t-editor-image-url').val(img ? img.getAttribute('src', 2) : 'http://').end()
                .find('#t-editor-image-title').val(img ? img.alt : '').end()
                .data('tWindow').open();

            $('#t-editor-image-url', dialog.element).focus().select();
        }
    }

    function LinkCommand(options) {
        Command.call(this, options.range);

        var attributes;

        this.async = true;

        var formatter = new LinkFormatter();

        this.exec = function () {
            var range = this.getRange();

            var collapsed = range.collapsed;

            range = this.lockRange(true);

            var nodes = textNodes(range);

            var initialText = null;

            var self = this;

            function apply(e) {
                var href = $('#t-editor-link-url', dialog.element).val();

                if (href && href != 'http://') {
                    attributes = { href: href };

                    var title = $('#t-editor-link-title', dialog.element).val();
                    if (title)
                        attributes.title = title;

                    var text = $('#t-editor-link-text', dialog.element).val();
                    if (text !== initialText)
                        attributes.innerHTML = text;

                    var target = $('#t-editor-link-target', dialog.element).val();
                    if (target != 'false')
                        attributes.target = '_blank';

                    formatter.apply(range, attributes);
                }
                close(e);
                if (self.change)
                    self.change();
            }

            function close(e) {
                e.preventDefault();
                dialog.destroy();

                windowFromDocument(documentFromRange(range)).focus();

                self.releaseRange(range);
            }

            var a = nodes.length ? formatter.finder.findSuitable(nodes[0]) : null;

            var shouldShowText = nodes.length <= 1 || (nodes.length == 2 && collapsed);

            var dialog = $t.window.create({
                title: "Insert link",
                html: new $.telerik.stringBuilder()
                    .cat('<div class="t-editor-dialog">')
                        .cat('<ol>')
                            .cat('<li class="t-form-text-row"><label for="t-editor-link-url">Web address</label><input type="text" id="t-editor-link-url"/></li>')
                            .catIf('<li class="t-form-text-row"><label for="t-editor-link-text">Text</label><input type="text" id="t-editor-link-text"/></li>', shouldShowText)
                            .cat('<li class="t-form-text-row"><label for="t-editor-link-title">Tooltip</label><input type="text" id="t-editor-link-title"/></li>')
                            .cat('<li class="t-form-checkbox-row"><input type="checkbox" id="t-editor-link-target"/><label for="t-editor-link-target">Open link in new window</label></li>')
                        .cat('</ol>')
                        .cat('<div class="t-button-wrapper">')
                            .cat('<button class="t-dialog-insert t-button t-state-default">Insert</button>')
                            .cat('&nbsp;or&nbsp;')
                            .cat('<a href="#" class="t-dialog-close">Close</a>')
                        .cat('</div>')
                    .cat('</div>')
                    .string(),
                modal: true, resizable: false, draggable: true,
                onClose: close
            })
                .css('display', 'none')
                .find('.t-dialog-insert').click(apply).end()
                .find('.t-dialog-close').click(close).end()
                .find('.t-form-text-row input').keydown(function (e) {
                    if (e.keyCode == 13)
                        apply(e);
                    else if (e.keyCode == 27)
                        close(e);
                }).end()
                .delegate('.t-button', 'mouseenter', $t.buttonHover)
                .delegate('.t-button', 'mouseleave', $t.buttonLeave)
                // IE < 8 returns absolute url if getAttribute is not used
                .find('#t-editor-link-url').val(a ? a.getAttribute('href', 2) : 'http://').end()
                .find('#t-editor-link-text').val(nodes.length > 0 ? (nodes.length == 1 ? nodes[0].nodeValue : nodes[0].nodeValue + nodes[1].nodeValue) : '').end()
                .find('#t-editor-link-title').val(a ? a.title : '').end()
                .find('#t-editor-link-target').val(a ? a.target == '_blank' : 'false').end()
                .data('tWindow').open();

            if (shouldShowText && nodes.length > 0)
                initialText = $('#t-editor-link-text', dialog.element).val();

            $('#t-editor-link-url', dialog.element).focus().select();
        },

        this.redo = function () {
            var range = this.lockRange(true);
            formatter.apply(range, attributes);
            this.releaseRange(range);
        }
    }

    function FormatCommand(options) {
        Command.call(this, options.range);
        var formatter = options.formatter();

        this.exec = function () {
            var range = this.lockRange(true);
            formatter.editor = this.editor;
            
            formatter.toggle(range);

            this.releaseRange(range)
        }
    }

    function GenericCommand(startRestorePoint, endRestorePoint) {
        var body = startRestorePoint.body;

        this.redo = function () {
            body.innerHTML = endRestorePoint.html;
            selectRange(endRestorePoint.toRange());
        }

        this.undo = function () {
            body.innerHTML = startRestorePoint.html;
            selectRange(startRestorePoint.toRange());
        }
    }

    function UndoRedoStack() {
        var stack = [], currentCommandIndex = -1;

        this.push = function (command) {
            stack = stack.slice(0, currentCommandIndex + 1);
            currentCommandIndex = stack.push(command) - 1;
        }

        this.undo = function () {
            if (this.canUndo())
                stack[currentCommandIndex--].undo();
        }

        this.redo = function () {
            if (this.canRedo())
                stack[++currentCommandIndex].redo();
        }

        this.canUndo = function () {
            return currentCommandIndex >= 0;
        }

        this.canRedo = function () {
            return currentCommandIndex != stack.length - 1;
        }
    }

    function removePendingFormats(editor) {
        if (editor.pendingFormats.length == 0) return;

        editor.pendingFormats.reverse();

        $.each(editor.pendingFormats, function() {
            for (var node = this.firstChild; node; node = node.nextSibling)
                while (node.nodeType == 3 && (charIndex = node.nodeValue.indexOf('\ufeff')) >= 0)
                    node.deleteData(charIndex, 1);
        });

        $.each(editor.pendingFormats, function() {
            if (this.innerHTML == '' && this.parentNode)
                dom.remove(this);
        });

        editor.pendingFormats = [];
    }

    function TypingHandler(editor) {
        this.keydown = function (e) {
            var keyboard = editor.keyboard;
            var isTypingKey = keyboard.isTypingKey(e);

            if (!isTypingKey)
                removePendingFormats(editor);

            if (isTypingKey && !keyboard.typingInProgress()) {
                this.startRestorePoint = new RestorePoint(editor.getRange());
                keyboard.startTyping($.proxy(function () {
                    this.endRestorePoint = new RestorePoint(editor.getRange());
                    editor.undoRedoStack.push(new GenericCommand(this.startRestorePoint, this.endRestorePoint));
                }, this));

                return true;
            }

            return false;
        }

        this.keyup = function (e) {
            var keyboard = editor.keyboard;

            if (keyboard.typingInProgress()) {
                keyboard.endTyping();
                return true;
            }

            return false;
        }
    }

    function SystemHandler(editor) {
        var systemCommandIsInProgress = false;

        this.createUndoCommand = function () {
            this.endRestorePoint = new RestorePoint(editor.getRange());
            editor.undoRedoStack.push(new GenericCommand(this.startRestorePoint, this.endRestorePoint));
            this.startRestorePoint = this.endRestorePoint;
        }

        this.changed = function () {
            if (this.startRestorePoint)
                return this.startRestorePoint.html != editor.body.innerHTML;

            return false;
        },

        this.keydown = function (e) {
            var keyboard = editor.keyboard;

            if (keyboard.isModifierKey(e)) {

                if (keyboard.typingInProgress())
                    keyboard.endTyping(true);

                this.startRestorePoint = new RestorePoint(editor.getRange());
                return true;
            }

            if (keyboard.isSystem(e)) {
                systemCommandIsInProgress = true;

                if (this.changed()) {
                    systemCommandIsInProgress = false;
                    this.createUndoCommand();
                }

                return true;
            }

            return false;
        }

        this.keyup = function (e) {
            if (systemCommandIsInProgress && this.changed()) {
                systemCommandIsInProgress = false;
                this.createUndoCommand(e);
                return true;
            }

            return false;
        }
    }

    function Keyboard(handlers) {
        var typingInProgress = false;
        var timeout;
        var onEndTyping;

        function isCharacter(keyCode) {
            return (keyCode >= 48 && keyCode <= 90) || (keyCode >= 96 && keyCode <= 111) ||
                (keyCode >= 186 && keyCode <= 192) || (keyCode >= 219 && keyCode <= 222);
        }

        this.toolFromShortcut = function (tools, e) {
            var key = String.fromCharCode(e.keyCode);

            for (var toolName in tools) {
                var tool = tools[toolName];

                if ((tool.key == key || tool.key == e.keyCode) && !!tool.ctrl == e.ctrlKey && !!tool.alt == e.altKey && !!tool.shift == e.shiftKey)
                    return toolName;
            }
        }

        this.isTypingKey = function (e) {
            var keyCode = e.keyCode;
            return (isCharacter(keyCode) && !e.ctrlKey && !e.altKey) || keyCode == 32 || keyCode == 13
            || keyCode == 8 || (keyCode == 46 && !e.shiftKey && !e.ctrlKey && !e.altKey);
        }

        this.isModifierKey = function (e) {
            var keyCode = e.keyCode;
            return (keyCode == 17 && !e.shiftKey && !e.altKey)
                    || (keyCode == 16 && !e.ctrlKey && !e.altKey)
                    || (keyCode == 18 && !e.ctrlKey && !e.shiftKey);
        }

        this.isSystem = function (e) {
            var keyCode = e.keyCode;
            return (e.ctrlKey || e.altKey || e.shiftKey) && !(keyCode >= 16 && keyCode <= 18) && keyCode != 90 && keyCode != 89;
        }

        this.startTyping = function (callback) {
            onEndTyping = callback;
            typingInProgress = true;
        }

        function stopTyping() {
            typingInProgress = false;
            onEndTyping();
        }

        this.endTyping = function (force) {
            this.clearTimeout();
            if (force)
                stopTyping();
            else
                timeout = window.setTimeout(stopTyping, 1000);
        }

        this.typingInProgress = function () {
            return typingInProgress;
        }

        this.clearTimeout = function () {
            window.clearTimeout(timeout);
        }

        function notify(e, what) {
            for (var i = 0; i < handlers.length; i++)
                if (handlers[i][what](e))
                    break;
        }

        this.keydown = function (e) {
            notify(e, 'keydown');
        }

        this.keyup = function (e) {
            notify(e, 'keyup');
        }
    }


    function Marker() {
        var caret;

        this.addCaret = function (range) {
            caret = dom.create(documentFromRange(range), 'span', { className: 't-marker' });
            range.insertNode(caret);
            range.selectNode(caret);
        }

        this.removeCaret = function (range) {
            var previous = caret.previousSibling;
            var startOffset = 0;
            
            if (previous)
                startOffset = isDataNode(previous) ? previous.nodeValue.length : findNodeIndex(previous);

            var container = caret.parentNode;
            var containerIndex = previous ? findNodeIndex(previous) : 0;

            dom.remove(caret);
            normalize(container);

            var node = container.childNodes[containerIndex];
            
            if (isDataNode(node))
                range.setStart(node, startOffset);
            else if (node)
                range[previous ? 'setStartAfter' : 'setStartBefore'](node);
            else {
                if (!$.browser.msie && container.innerHTML == '')
                    container.innerHTML = '<br _moz_dirty="" />';
                
                range.selectNodeContents(container);
            }
            range.collapse(true);
        }

        this.add = function (range, expand) {
            if (expand && range.collapsed) {
                this.addCaret(range);
                range = RangeUtils.expand(range);
            }

            var rangeBoundary = range.cloneRange();

            rangeBoundary.collapse(false);
            this.end = dom.create(documentFromRange(range), 'span', { className: 't-marker' });
            rangeBoundary.insertNode(this.end);

            rangeBoundary = range.cloneRange();
            rangeBoundary.collapse(true);
            this.start = this.end.cloneNode(true);
            rangeBoundary.insertNode(this.start);

            range.setStartBefore(this.start);
            range.setEndAfter(this.end);

            normalize(range.commonAncestorContainer);

            return range;
        }

        this.remove = function (range) {
            var start = this.start, end = this.end;

            normalize(range.commonAncestorContainer);

            while (!start.nextSibling && start.parentNode) start = start.parentNode;
            while (!end.previousSibling && end.parentNode) end = end.parentNode;

            var shouldNormalizeStart = (start.previousSibling && start.previousSibling.nodeType == 3)
                                    && (start.nextSibling && start.nextSibling.nodeType == 3);

            var shouldNormalizeEnd = (end.previousSibling && end.previousSibling.nodeType == 3)
                                  && (end.nextSibling && end.nextSibling.nodeType == 3);

            start = start.nextSibling;
            end = end.previousSibling;

            var collapsed = false;
            var collapsedToStart = false;
            // collapsed range
            if (start == this.end) {
                collapsedToStart = !!this.start.previousSibling;
                start = end = this.start.previousSibling || this.end.nextSibling;
                collapsed = true;
            }

            dom.remove(this.start);
            dom.remove(this.end);

            var startOffset = collapsed ? isDataNode(start) ? start.nodeValue.length : start.childNodes.length : 0;
            var endOffset = isDataNode(end) ? end.nodeValue.length : end.childNodes.length;

            if (start.nodeType == 3)
                while (start.previousSibling && start.previousSibling.nodeType == 3) {
                    start = start.previousSibling;
                    startOffset += start.nodeValue.length;
                }

            if (end.nodeType == 3)
                while (end.previousSibling && end.previousSibling.nodeType == 3) {
                    end = end.previousSibling;
                    endOffset += end.nodeValue.length;
                }
            var startIndex = findNodeIndex(start), startParent = start.parentNode;
            var endIndex = findNodeIndex(end), endParent = end.parentNode;

            for (var startPointer = start; startPointer.previousSibling; startPointer = startPointer.previousSibling)
                if (startPointer.nodeType == 3 && startPointer.previousSibling.nodeType == 3) startIndex--;

            for (var endPointer = end; endPointer.previousSibling; endPointer = endPointer.previousSibling)
                if (endPointer.nodeType == 3 && endPointer.previousSibling.nodeType == 3) endIndex--;

            normalize(startParent);

            if (start.nodeType == 3)
                start = startParent.childNodes[startIndex];

            normalize(endParent);
            if (end.nodeType == 3)
                end = endParent.childNodes[endIndex];

            if (collapsed) {
                if (start.nodeType == 3)
                    range.setStart(start, startOffset);
                else
                    range[collapsedToStart ? 'setStartAfter' : 'setStartBefore'](start);
                
                range.collapse(true);

            } else {
                if (start.nodeType == 3)
                    range.setStart(start, startOffset);
                else
                    range.setStartBefore(start);

                if (end.nodeType == 3)
                    range.setEnd(end, endOffset);
                else
                    range.setEndAfter(end);
            }
            if (caret)
                this.removeCaret(range);
        }
    }

    var boundary = /[\u0009-\u000d]|\u0020|\u00a0|\ufeff|\.|,|;|:|!|\(|\)|\?/;

    var RangeUtils = {

        image: function (range) {
            var nodes = [];

            new RangeIterator(range).traverse(function (node) {
                if (dom.is(node, 'img'))
                    nodes.push(node);
            });

            if (nodes.length == 1)
                return nodes[0];
        },

        expand: function (range) {
            var result = range.cloneRange();

            var startContainer = result.startContainer.childNodes[result.startOffset == 0 ? 0 : result.startOffset - 1];
            var endContainer = result.endContainer.childNodes[result.endOffset];

            if (!isDataNode(startContainer) || !isDataNode(endContainer))
                return result;

            var beforeCaret = startContainer.nodeValue;
            var afterCaret = endContainer.nodeValue;

            if (beforeCaret == '' || afterCaret == '')
                return result;

            var startOffset = beforeCaret.split('').reverse().join('').search(boundary);
            var endOffset = afterCaret.search(boundary);

            if (startOffset == 0 || endOffset == 0)
                return result;

            endOffset = endOffset == -1 ? afterCaret.length : endOffset;
            startOffset = startOffset == -1 ? 0 : beforeCaret.length - startOffset;

            result.setStart(startContainer, startOffset);
            result.setEnd(endContainer, endOffset);

            return result;
        },

        isExpandable: function (range) {
            var node = range.startContainer;
            var document = documentFromRange(range);

            if (node == document || node == document.body)
                return false;

            var result = range.cloneRange();

            var value = node.nodeValue;
            if (!value)
                return false;

            var beforeCaret = value.substring(0, result.startOffset);
            var afterCaret = value.substring(result.startOffset);

            var startOffset = 0, endOffset = 0;

            if (beforeCaret != '')
                startOffset = beforeCaret.split('').reverse().join('').search(boundary);

            if (afterCaret != '')
                endOffset = afterCaret.search(boundary);

            return startOffset != 0 && endOffset != 0;
        }
    }

    $.extend($t.editor, {
        Dom: dom,
        Marker: Marker,
        RangeUtils: RangeUtils,
        LinkFormatter: LinkFormatter,
        InlineFormatter: InlineFormatter,
        ListFormatter: ListFormatter,
        BlockFormatter: BlockFormatter,
        ListFormatFinder: ListFormatFinder,
        BlockFormatFinder: BlockFormatFinder,
        LinkFormatFinder: LinkFormatFinder,
        InlineFormatFinder: InlineFormatFinder,
        RangeEnumerator: RangeEnumerator,
        RestorePoint: RestorePoint,
        ParagraphCommand: ParagraphCommand,
        NewLineCommand: NewLineCommand,
        ListCommand: ListCommand,
        ImageCommand: ImageCommand,
        LinkCommand: LinkCommand,
        UnlinkCommand: UnlinkCommand,
        FormatCommand: FormatCommand,
        GenericCommand: GenericCommand,
        UndoRedoStack: UndoRedoStack,
        TypingHandler: TypingHandler,
        SystemHandler: SystemHandler,
        Keyboard: Keyboard
    });

    // public api
    $t.editor.prototype = {
        value: function (html) {
            if (html === undefined) return domToXhtml(this.body);

            // Some browsers do not allow setting CDATA sections through innerHTML so we encode them as comments
            html = html.replace(/<!\[CDATA\[(.*)?\]\]>/g, '<!--[CDATA[$1]]-->');

            // Encode script tags to avoid execution and lost content (IE)
            html = html.replace(/<script([^>]*)>(.*)?<\/script>/ig, '<telerik:script $1>$2<\/telerik:script>');

            if ($.browser.msie) {
                // Internet Explorer removes comments from the beginning of the html
                html = '<br/>' + html;

                // IE < 8 makes href and src attributes absolute
                html = html.replace(/href\s*=\s*(?:'|")?([^'">\s]*)(?:'|")?/, 'originalhref="$1"');
                html = html.replace(/src\s*=\s*(?:'|")?([^'">\s]*)(?:'|")?/, 'originalsrc="$1"');

                this.body.innerHTML = html;
                dom.remove(this.body.firstChild);

                $('script,link,img,a', this.body).each(function () {
                    if (this['originalhref']) {
                        this.setAttribute('href', this['originalhref']);
                        this.removeAttribute('originalhref');
                    }
                    if (this['originalsrc']) {
                        this.setAttribute('src', this['originalsrc']);
                        this.removeAttribute('originalsrc');
                    }
                })
            } else {
                this.body.innerHTML = html;
            }

            this.update();
        },

        focus: function () {
            this.window.focus();
        },

        update: function (value) {
            this.textarea.value = value || this.encodedValue();
        },

        encodedValue: function () {
            return dom.encode(this.value());
        },

        createRange: function (document) {
            return createRange(document || this.document);
        },

        getSelection: function () {
            return selectionFromDocument(this.document);
        },
        
        selectRange: function(range) {
            var selection = this.getSelection();
            selection.removeAllRanges();
            selection.addRange(range);
        },

        getRange: function () {
            var selection = this.getSelection();
            return selection.rangeCount > 0 ? selection.getRangeAt(0) : this.createRange();
        },

        exec: function (name, params) {
            var tool = this.tools[name.toLowerCase()];

            if (tool) {
                var range = this.getRange();
                var command = tool.command ? tool.command({ range: range }) : null;

                $t.trigger(this.element, 'execute', { name: name, command: command });

                if (/undo|redo/i.test(name)) {
                    this.undoRedoStack[name.toLowerCase()]();
                } else {
                    this.undoRedoStack.push(command);

                    command.editor = this;
                    command.exec();

                    if (command.async) {
                        command.change = $.proxy(function () { selectionChanged(this); }, this);
                        return;
                    }
                }

                selectionChanged(this);
            }
        }
    }

    $.fn.tEditor = function (options) {
        return $t.create(this, {
            name: 'tEditor',
            init: function (element, options) {
                return new $t.editor(element, options);
            },
            options: options
        });
    }

    var formats = {
        bold: [
            { tags: ['strong'] },
            { tags: ['span'], attr: { style: { fontWeight: 'bold'}} }
        ],

        italic: [
            { tags: ['em'] },
            { tags: ['span'], attr: { style: { fontStyle: 'italic'}} }
        ],

        underline: [{ tags: ['span'], attr: { style: { textDecoration: 'underline'}}}],

        strikeThrough: [
            { tags: ['del'] },
            { tags: ['span'], attr: { style: { textDecoration: 'line-through'}} }
        ],

        justifyLeft: [
            { tags: blockElements, attr: { style: { textAlign: 'left'}} },
            { tags: ['img'], attr: { style: { 'float': 'left'}} }
        ],

        justifyCenter: [
            { tags: blockElements, attr: { style: { textAlign: 'center'}} },
            { tags: ['img'], attr: { style: { display: 'block', marginLeft: 'auto', marginRight: 'auto'}} }
        ],

        justifyRight: [
            { tags: blockElements, attr: { style: { textAlign: 'right'}} },
            { tags: ['img'], attr: { style: { 'float': 'right'}} }
        ],

        justifyFull: [
            { tags: blockElements, attr: { style: { textAlign: 'justify'}} }
        ]
    };

    function createFormat(shortcut, format, type) {
        return $.extend(shortcut, {
            command: function (options) {
                return new FormatCommand($.extend(options, {
                    format: format,
                    formatter: function () {
                        return new $t.editor[type + 'Formatter'](format);
                    }
                }))
            },
            finder: function () {
                return new $t.editor[type + 'FormatFinder'](format);
            }
        });
    }

    function inlineFormat(shortcut, format) {
        return createFormat($.extend(shortcut, { inline: true }), format, 'Inline');
    }

    function blockFormat(shortcut, format) {
        return createFormat(shortcut, format, 'Block');
    }

    function formatByName(name, format) {
        for (var i = 0; i < format.length; i++)
            if ($.inArray(name, format[i].tags) >= 0)
                return format[i];
    }

    $.fn.tEditor.defaults = {
        plugins: [],
        localization: {},
        formats: formats,
        tools: {
            bold: inlineFormat({ key: 'B', ctrl: true, description: 'Bold' }, formats.bold),
            italic: inlineFormat({ key: 'I', ctrl: true, description: 'Italic' }, formats.italic),
            underline: inlineFormat({ key: 'U', ctrl: true, description: 'Underline' }, formats.underline),
            strikethrough: inlineFormat({ description: 'Strikethrough' }, formats.strikeThrough),
            undo: { key: 'Z', ctrl: true },
            redo: { key: 'Y', ctrl: true },
            newline: { key: 13, shift: true, command: function (options) { return new NewLineCommand(options) } },
            paragraph: { key: 13, command: function (options) { return new ParagraphCommand(options) } },
            justifycenter: blockFormat({ description: 'Center text' }, formats.justifyCenter),
            justifyleft: blockFormat({ description: 'Align text left' }, formats.justifyLeft),
            justifyright: blockFormat({ description: 'Align text right' }, formats.justifyRight),
            justifyfull: blockFormat({ description: 'Justify' }, formats.justifyFull),
            insertunorderedlist: { description: 'Insert unordered list', finder: function () { return new ListFormatFinder('ul') }, command: function (options) { return new ListCommand($.extend(options, { tag: 'ul' })) } },
            insertorderedlist: { description: 'Insert ordered list', finder: function () { return new ListFormatFinder('ol') }, command: function (options) { return new ListCommand($.extend(options, { tag: 'ol' })) } },
            createlink: { key: 'K', ctrl: true, description: 'Insert hyperlink', finder: function () { return { isFormatted: function () { return false } } }, command: function (options) { return new LinkCommand(options) } },
            unlink: { key: 'K', ctrl: true, shift: true, description: 'Remove hyperlink', finder: function () { return { isFormatted: function () { return false } } }, command: function (options) { return new UnlinkCommand(options) } },
            insertimage: { description: 'Insert image', finder: function () { return { isFormatted: function () { return false } } }, command: function (options) { return new ImageCommand(options) } }
        }
    }
})(jQuery);